Otključajte robusne Node.js operacije s datotekama pomoću TypeScripta. Vodič za sinkrone, asinkrone i stream metode, uz sigurnost tipova i obradu grešaka.
Ovladavanje TypeScript datotečnim sustavom: Node.js operacije s datotekama uz sigurnost tipova za globalne programere
U prostranom krajoliku modernog razvoja softvera, Node.js se ističe kao moćno okruženje za izgradnju skalabilnih poslužiteljskih aplikacija, alata za naredbeni redak i još mnogo toga. Temeljni aspekt mnogih Node.js aplikacija uključuje interakciju s datotečnim sustavom – čitanje, pisanje, stvaranje i upravljanje datotekama i direktorijima. Iako JavaScript pruža fleksibilnost za obavljanje tih operacija, uvođenje TypeScripta podiže to iskustvo donoseći statičku provjeru tipova, poboljšane alate i, u konačnici, veću pouzdanost i održivost vašeg koda za rad s datotečnim sustavom.
Ovaj sveobuhvatni vodič namijenjen je globalnoj publici programera, bez obzira na njihovu kulturnu pozadinu ili geografsku lokaciju, koji žele ovladati Node.js operacijama s datotekama uz robusnost koju nudi TypeScript. Zaronit ćemo u srž `fs` modula, istražiti njegove različite sinkrone i asinkrone paradigme, proučiti moderne API-je temeljene na `Promise`-ima i otkriti kako TypeScriptov sustav tipova može značajno smanjiti uobičajene greške i poboljšati jasnoću vašeg koda.
Kamen temeljac: Razumijevanje Node.js datotečnog sustava (`fs`)
Node.js `fs` modul pruža API za interakciju s datotečnim sustavom na način modeliran prema standardnim POSIX funkcijama. Nudi širok spektar metoda, od osnovnog čitanja i pisanja datoteka do složenih manipulacija direktorijima i praćenja datoteka. Tradicionalno, ove operacije su se obavljale pomoću povratnih poziva (callbacks), što je u složenim scenarijima dovodilo do zloglasnog "callback hell-a". S evolucijom Node.js-a, `Promise`-i i `async/await` postali su preferirani obrasci za asinkrone operacije, čineći kod čitljivijim i lakšim za upravljanje.
Zašto koristiti TypeScript za operacije s datotečnim sustavom?
Iako Node.js `fs` modul savršeno funkcionira s čistim JavaScriptom, integracija TypeScripta donosi nekoliko uvjerljivih prednosti:
- Sigurnost tipova: Otkriva uobičajene greške poput netočnih tipova argumenata, nedostajućih parametara ili neočekivanih povratnih vrijednosti tijekom kompajliranja, prije nego što se vaš kod uopće pokrene. To je neprocjenjivo, posebno kada se radi s različitim kodiranjima datoteka, zastavicama i `Buffer` objektima.
- Poboljšana čitljivost: Eksplicitne anotacije tipova jasno pokazuju kakve podatke funkcija očekuje i što će vratiti, poboljšavajući razumijevanje koda za programere u različitim timovima.
- Bolji alati i automatsko dovršavanje: IDE okruženja (poput VS Code-a) koriste TypeScriptove definicije tipova kako bi pružili inteligentno automatsko dovršavanje, prijedloge parametara i ugrađenu dokumentaciju, značajno povećavajući produktivnost.
- Pouzdanost pri refaktoriranju: Kada promijenite sučelje ili potpis funkcije, TypeScript odmah označava sva pogođena područja, čineći refaktoriranje velikih razmjera manje podložnim greškama.
- Globalna dosljednost: Osigurava dosljedan stil kodiranja i razumijevanje struktura podataka u međunarodnim razvojnim timovima, smanjujući nejasnoće.
Sinkrone naspram asinkronih operacija: Globalna perspektiva
Razumijevanje razlike između sinkronih i asinkronih operacija ključno je, posebno pri izradi aplikacija za globalnu implementaciju gdje su performanse i odzivnost od presudne važnosti. Većina funkcija `fs` modula dolazi u sinkronim i asinkronim varijantama. Kao opće pravilo, asinkrone metode su preferirane za neblokirajuće I/O operacije, koje su ključne za održavanje odzivnosti vašeg Node.js poslužitelja.
- Asinkrone (neblokirajuće): Ove metode uzimaju povratnu funkciju (callback) kao zadnji argument ili vraćaju `Promise`. One pokreću operaciju na datotečnom sustavu i odmah se vraćaju, dopuštajući izvršavanje drugog koda. Kada se operacija završi, poziva se povratna funkcija (ili se `Promise` ispunjava/odbija). Ovo je idealno za poslužiteljske aplikacije koje obrađuju više istovremenih zahtjeva korisnika diljem svijeta, jer sprječava zamrzavanje poslužitelja dok čeka da se operacija s datotekom završi.
- Sinkrone (blokirajuće): Ove metode izvršavaju operaciju u potpunosti prije povratka. Iako su jednostavnije za kodiranje, one blokiraju Node.js event loop, sprječavajući izvršavanje bilo kojeg drugog koda dok se operacija s datotečnim sustavom ne završi. To može dovesti do značajnih uskih grla u performansama i neodzivnih aplikacija, posebno u okruženjima s velikim prometom. Koristite ih štedljivo, obično za logiku pokretanja aplikacije ili jednostavne skripte gdje je blokiranje prihvatljivo.
Osnovni tipovi operacija s datotekama u TypeScriptu
Zaronimo u praktičnu primjenu TypeScripta s uobičajenim operacijama na datotečnom sustavu. Koristit ćemo ugrađene definicije tipova za Node.js, koje su obično dostupne putem `@types/node` paketa.
Za početak, provjerite imate li instaliran TypeScript i Node.js tipove u svom projektu:
npm install typescript @types/node --save-dev
Vaš `tsconfig.json` bi trebao biti prikladno konfiguriran, na primjer:
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
Čitanje datoteka: `readFile`, `readFileSync` i Promises API
Čitanje sadržaja iz datoteka je temeljna operacija. TypeScript pomaže osigurati da ispravno rukujete putanjama datoteka, kodiranjima i potencijalnim greškama.
Asinkrono čitanje datoteke (temeljeno na povratnim pozivima)
Funkcija `fs.readFile` je radni konj za asinkrono čitanje datoteka. Prima putanju, opcionalno kodiranje i povratnu funkciju. TypeScript osigurava da su argumenti povratne funkcije ispravno tipizirani (`Error | null`, `Buffer | string`).
import * as fs from 'fs';
const filePath: string = 'data/example.txt';
fs.readFile(filePath, 'utf8', (err: NodeJS.ErrnoException | null, data: string) => {
if (err) {
// Zabilježite grešku za međunarodno otklanjanje grešaka, npr., 'Datoteka nije pronađena'
console.error(`Greška pri čitanju datoteke '${filePath}': ${err.message}`);
return;
}
// Obradite sadržaj datoteke, osiguravajući da je string prema 'utf8' kodiranju
console.log(`Sadržaj datoteke (${filePath}):\n${data}`);
});
// Primjer: Čitanje binarnih podataka (nije navedeno kodiranje)
const binaryFilePath: string = 'data/image.png';
fs.readFile(binaryFilePath, (err: NodeJS.ErrnoException | null, data: Buffer) => {
if (err) {
console.error(`Greška pri čitanju binarne datoteke '${binaryFilePath}': ${err.message}`);
return;
}
// 'data' je ovdje Buffer, spreman za daljnju obradu (npr., streamanje klijentu)
console.log(`Pročitano ${data.byteLength} bajtova iz ${binaryFilePath}`);
});
Sinkrono čitanje datoteke
`fs.readFileSync` blokira event loop. Njegov povratni tip je `Buffer` ili `string`, ovisno o tome je li navedeno kodiranje. TypeScript to ispravno zaključuje.
import * as fs from 'fs';
const syncFilePath: string = 'data/sync_example.txt';
try {
const content: string = fs.readFileSync(syncFilePath, 'utf8');
console.log(`Sinkrono pročitan sadržaj (${syncFilePath}):\n${content}`);
} catch (error: any) {
console.error(`Greška pri sinkronom čitanju za '${syncFilePath}': ${error.message}`);
}
Čitanje datoteke temeljeno na `Promise`-ima (`fs/promises`)
Moderni `fs/promises` API nudi čišće sučelje temeljeno na `Promise`-ima, što se toplo preporučuje za asinkrone operacije. TypeScript se ovdje ističe, posebno s `async/await`.
import * as fsPromises from 'fs/promises';
async function readTextFile(path: string): Promise
Pisanje datoteka: `writeFile`, `writeFileSync` i zastavice
Pisanje podataka u datoteke jednako je ključno. TypeScript pomaže u upravljanju putanjama datoteka, tipovima podataka (string ili Buffer), kodiranjem i zastavicama za otvaranje datoteka.
Asinkrono pisanje datoteke
`fs.writeFile` se koristi za pisanje podataka u datoteku, zamjenjujući datoteku ako već postoji po zadanom. To ponašanje možete kontrolirati pomoću `flags` (zastavica).
import * as fs from 'fs';
const outputFilePath: string = 'data/output.txt';
const fileContent: string = 'Ovo je novi sadržaj zapisan pomoću TypeScripta.';
fs.writeFile(outputFilePath, fileContent, 'utf8', (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Greška pri pisanju datoteke '${outputFilePath}': ${err.message}`);
return;
}
console.log(`Datoteka '${outputFilePath}' uspješno zapisana.`);
});
// Primjer s Buffer podacima
const bufferContent: Buffer = Buffer.from('Primjer binarnih podataka');
const binaryOutputFilePath: string = 'data/binary_output.bin';
fs.writeFile(binaryOutputFilePath, bufferContent, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Greška pri pisanju binarne datoteke '${binaryOutputFilePath}': ${err.message}`);
return;
}
console.log(`Binarna datoteka '${binaryOutputFilePath}' uspješno zapisana.`);
});
Sinkrono pisanje datoteke
`fs.writeFileSync` blokira event loop dok se operacija pisanja ne završi.
import * as fs from 'fs';
const syncOutputFilePath: string = 'data/sync_output.txt';
try {
fs.writeFileSync(syncOutputFilePath, 'Sinkrono zapisan sadržaj.', 'utf8');
console.log(`Datoteka '${syncOutputFilePath}' sinkrono zapisana.`);
} catch (error: any) {
console.error(`Greška pri sinkronom pisanju za '${syncOutputFilePath}': ${error.message}`);
}
Pisanje datoteke temeljeno na `Promise`-ima (`fs/promises`)
Moderni pristup s `async/await` i `fs/promises` često je čišći za upravljanje asinkronim pisanjem.
import * as fsPromises from 'fs/promises';
import { constants as fsConstants } from 'fs'; // Za zastavice
async function writeDataToFile(path: string, data: string | Buffer): Promise
Važne zastavice:
- `'w'` (zadano): Otvori datoteku za pisanje. Datoteka se stvara (ako ne postoji) ili se njezin sadržaj briše (ako postoji).
- `'w+'`: Otvori datoteku za čitanje i pisanje. Datoteka se stvara (ako ne postoji) ili se njezin sadržaj briše (ako postoji).
- `'a'` (dodavanje): Otvori datoteku za dodavanje na kraj. Datoteka se stvara ako ne postoji.
- `'a+'`: Otvori datoteku za čitanje i dodavanje na kraj. Datoteka se stvara ako ne postoji.
- `'r'` (čitanje): Otvori datoteku za čitanje. Dolazi do iznimke ako datoteka ne postoji.
- `'r+'`: Otvori datoteku za čitanje i pisanje. Dolazi do iznimke ako datoteka ne postoji.
- `'wx'` (isključivo pisanje): Kao `'w'`, ali ne uspijeva ako putanja postoji.
- `'ax'` (isključivo dodavanje): Kao `'a'`, ali ne uspijeva ako putanja postoji.
Dodavanje u datoteke: `appendFile`, `appendFileSync`
Kada trebate dodati podatke na kraj postojeće datoteke bez prepisivanja njezinog sadržaja, `appendFile` je vaš izbor. Ovo je posebno korisno za bilježenje (logging), prikupljanje podataka ili revizijske tragove.
Asinkrono dodavanje
import * as fs from 'fs';
const logFilePath: string = 'data/app_logs.log';
function logMessage(message: string): void {
const timestamp: string = new Date().toISOString();
const logEntry: string = `${timestamp} - ${message}\n`;
fs.appendFile(logFilePath, logEntry, 'utf8', (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Greška pri dodavanju u log datoteku '${logFilePath}': ${err.message}`);
return;
}
console.log(`Poruka zabilježena u '${logFilePath}'.`);
});
}
logMessage('Korisnik "Alice" se prijavio.');
setTimeout(() => logMessage('Pokrenuto ažuriranje sustava.'), 50);
logMessage('Uspostavljena veza s bazom podataka.');
Sinkrono dodavanje
import * as fs from 'fs';
const syncLogFilePath: string = 'data/sync_app_logs.log';
function logMessageSync(message: string): void {
const timestamp: string = new Date().toISOString();
const logEntry: string = `${timestamp} - ${message}\n`;
try {
fs.appendFileSync(syncLogFilePath, logEntry, 'utf8');
console.log(`Poruka sinkrono zabilježena u '${syncLogFilePath}'.`);
} catch (error: any) {
console.error(`Sinkrona greška pri dodavanju u log datoteku '${syncLogFilePath}': ${error.message}`);
}
}
logMessageSync('Aplikacija pokrenuta.');
logMessageSync('Konfiguracija učitana.');
Dodavanje temeljeno na `Promise`-ima (`fs/promises`)
import * as fsPromises from 'fs/promises';
const promiseLogFilePath: string = 'data/promise_app_logs.log';
async function logMessagePromise(message: string): Promise
Brisanje datoteka: `unlink`, `unlinkSync`
Uklanjanje datoteka s datotečnog sustava. TypeScript pomaže osigurati da prosljeđujete valjanu putanju i ispravno obrađujete greške.
Asinkrono brisanje
import * as fs from 'fs';
const fileToDeletePath: string = 'data/temp_to_delete.txt';
// Prvo, stvorite datoteku kako biste osigurali da postoji za demo brisanja
fs.writeFile(fileToDeletePath, 'Privremeni sadržaj.', 'utf8', (err) => {
if (err) {
console.error('Greška pri stvaranju datoteke za demo brisanja:', err);
return;
}
console.log(`Datoteka '${fileToDeletePath}' stvorena za demo brisanja.`);
fs.unlink(fileToDeletePath, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Greška pri brisanju datoteke '${fileToDeletePath}': ${err.message}`);
return;
}
console.log(`Datoteka '${fileToDeletePath}' uspješno izbrisana.`);
});
});
Sinkrono brisanje
import * as fs from 'fs';
const syncFileToDeletePath: string = 'data/sync_temp_to_delete.txt';
try {
fs.writeFileSync(syncFileToDeletePath, 'Sinkroni privremeni sadržaj.', 'utf8');
console.log(`Datoteka '${syncFileToDeletePath}' stvorena.`);
fs.unlinkSync(syncFileToDeletePath);
console.log(`Datoteka '${syncFileToDeletePath}' sinkrono izbrisana.`);
} catch (error: any) {
console.error(`Greška pri sinkronom brisanju za '${syncFileToDeletePath}': ${error.message}`);
}
Brisanje temeljeno na `Promise`-ima (`fs/promises`)
import * as fsPromises from 'fs/promises';
const promiseFileToDeletePath: string = 'data/promise_temp_to_delete.txt';
async function deleteFile(path: string): Promise
Provjera postojanja datoteke i dozvola: `existsSync`, `access`, `accessSync`
Prije rada s datotekom, možda ćete trebati provjeriti postoji li ili ima li trenutni proces potrebne dozvole. TypeScript pomaže pružanjem tipova za `mode` parametar.
Sinkrona provjera postojanja
`fs.existsSync` je jednostavna, sinkrona provjera. Iako je praktična, ima ranjivost na "race condition" (datoteka se može izbrisati između `existsSync` i naknadne operacije), pa je često bolje koristiti `fs.access` za kritične operacije.
import * as fs from 'fs';
const checkFilePath: string = 'data/example.txt';
if (fs.existsSync(checkFilePath)) {
console.log(`Datoteka '${checkFilePath}' postoji.`);
} else {
console.log(`Datoteka '${checkFilePath}' ne postoji.`);
}
Asinkrona provjera dozvola (`fs.access`)
`fs.access` testira korisničke dozvole za datoteku ili direktorij naveden u `path`. Asinkrona je i prima `mode` argument (npr., `fs.constants.F_OK` za postojanje, `R_OK` za čitanje, `W_OK` za pisanje, `X_OK` za izvršavanje).
import * as fs from 'fs';
import { constants } from 'fs';
const accessFilePath: string = 'data/example.txt';
fs.access(accessFilePath, constants.F_OK, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Datoteka '${accessFilePath}' ne postoji ili je pristup odbijen.`);
return;
}
console.log(`Datoteka '${accessFilePath}' postoji.`);
});
fs.access(accessFilePath, constants.R_OK | constants.W_OK, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Datoteka '${accessFilePath}' nije čitljiva/pisiva ili je pristup odbijen: ${err.message}`);
return;
}
console.log(`Datoteka '${accessFilePath}' je čitljiva i pisiva.`);
});
Provjera dozvola temeljena na `Promise`-ima (`fs/promises`)
import * as fsPromises from 'fs/promises';
import { constants } from 'fs';
async function checkFilePermissions(path: string, mode: number): Promise
Dohvaćanje informacija o datoteci: `stat`, `statSync`, `fs.Stats`
Obitelj funkcija `fs.stat` pruža detaljne informacije o datoteci ili direktoriju, kao što su veličina, datum stvaranja, datum izmjene i dozvole. TypeScriptovo `fs.Stats` sučelje čini rad s tim podacima visoko strukturiranim i pouzdanim.
Asinkroni `stat`
import * as fs from 'fs';
import { Stats } from 'fs';
const statFilePath: string = 'data/example.txt';
fs.stat(statFilePath, (err: NodeJS.ErrnoException | null, stats: Stats) => {
if (err) {
console.error(`Greška pri dohvaćanju statistike za '${statFilePath}': ${err.message}`);
return;
}
console.log(`Statistika za '${statFilePath}':`);
console.log(` Je li datoteka: ${stats.isFile()}`);
console.log(` Je li direktorij: ${stats.isDirectory()}`);
console.log(` Veličina: ${stats.size} bajtova`);
console.log(` Vrijeme stvaranja: ${stats.birthtime.toISOString()}`);
console.log(` Zadnja izmjena: ${stats.mtime.toISOString()}`);
});
`stat` temeljen na `Promise`-ima (`fs/promises`)
import * as fsPromises from 'fs/promises';
import { Stats } from 'fs'; // I dalje koristite Stats sučelje iz 'fs' modula
async function getFileStats(path: string): Promise
Operacije s direktorijima uz TypeScript
Upravljanje direktorijima čest je zahtjev za organiziranje datoteka, stvaranje pohrane specifične za aplikaciju ili rukovanje privremenim podacima. TypeScript pruža robusno tipiziranje za ove operacije.
Stvaranje direktorija: `mkdir`, `mkdirSync`
Funkcija `fs.mkdir` koristi se za stvaranje novih direktorija. `recursive` opcija je iznimno korisna za stvaranje nadređenih direktorija ako već ne postoje, oponašajući ponašanje `mkdir -p` u Unix-sličnim sustavima.
Asinkrono stvaranje direktorija
import * as fs from 'fs';
const newDirPath: string = 'data/new_directory';
const recursiveDirPath: string = 'data/nested/path/to/create';
// Stvori jedan direktorij
fs.mkdir(newDirPath, (err: NodeJS.ErrnoException | null) => {
if (err) {
// Zanemari EEXIST grešku ako direktorij već postoji
if (err.code === 'EEXIST') {
console.log(`Direktorij '${newDirPath}' već postoji.`);
} else {
console.error(`Greška pri stvaranju direktorija '${newDirPath}': ${err.message}`);
}
return;
}
console.log(`Direktorij '${newDirPath}' uspješno stvoren.`);
});
// Stvori ugniježđene direktorije rekurzivno
fs.mkdir(recursiveDirPath, { recursive: true }, (err: NodeJS.ErrnoException | null) => {
if (err) {
if (err.code === 'EEXIST') {
console.log(`Direktorij '${recursiveDirPath}' već postoji.`);
} else {
console.error(`Greška pri stvaranju rekurzivnog direktorija '${recursiveDirPath}': ${err.message}`);
}
return;
}
console.log(`Rekurzivni direktoriji '${recursiveDirPath}' uspješno stvoreni.`);
});
Stvaranje direktorija temeljeno na `Promise`-ima (`fs/promises`)
import * as fsPromises from 'fs/promises';
async function createDirectory(path: string, recursive: boolean = false): Promise
Čitanje sadržaja direktorija: `readdir`, `readdirSync`, `fs.Dirent`
Za ispis datoteka i poddirektorija unutar zadanog direktorija, koristite `fs.readdir`. Opcija `withFileTypes` je moderan dodatak koji vraća `fs.Dirent` objekte, pružajući detaljnije informacije izravno bez potrebe za `stat` pozivom za svaki unos pojedinačno.
Asinkrono čitanje direktorija
import * as fs from 'fs';
const readDirPath: string = 'data';
fs.readdir(readDirPath, (err: NodeJS.ErrnoException | null, files: string[]) => {
if (err) {
console.error(`Greška pri čitanju direktorija '${readDirPath}': ${err.message}`);
return;
}
console.log(`Sadržaj direktorija '${readDirPath}':`);
files.forEach(file => {
console.log(` - ${file}`);
});
});
// S opcijom `withFileTypes`
fs.readdir(readDirPath, { withFileTypes: true }, (err: NodeJS.ErrnoException | null, dirents: fs.Dirent[]) => {
if (err) {
console.error(`Greška pri čitanju direktorija s tipovima datoteka '${readDirPath}': ${err.message}`);
return;
}
console.log(`Sadržaj direktorija '${readDirPath}' (s tipovima):`);
dirents.forEach(dirent => {
const type: string = dirent.isFile() ? 'Datoteka' : dirent.isDirectory() ? 'Direktorij' : 'Ostalo';
console.log(` - ${dirent.name} (${type})`);
});
});
Čitanje direktorija temeljeno na `Promise`-ima (`fs/promises`)
import * as fsPromises from 'fs/promises';
import { Dirent } from 'fs'; // I dalje koristite Dirent sučelje iz 'fs' modula
async function listDirectoryContents(path: string): Promise
Brisanje direktorija: `rmdir` (zastarjelo), `rm`, `rmSync`
Node.js je razvio svoje metode za brisanje direktorija. `fs.rmdir` je sada uvelike zamijenjen s `fs.rm` za rekurzivna brisanja, nudeći robusniji i dosljedniji API.
Asinkrono brisanje direktorija (`fs.rm`)
Funkcija `fs.rm` (dostupna od Node.js 14.14.0) preporučeni je način za uklanjanje datoteka i direktorija. Opcija `recursive: true` ključna je za brisanje direktorija koji nisu prazni.
import * as fs from 'fs';
const dirToDeletePath: string = 'data/dir_to_delete';
const nestedDirToDeletePath: string = 'data/nested_dir/sub';
// Priprema: Stvorite direktorij s datotekom unutra za demo rekurzivnog brisanja
fs.mkdir(nestedDirToDeletePath, { recursive: true }, (err) => {
if (err && err.code !== 'EEXIST') {
console.error('Greška pri stvaranju ugniježđenog direktorija za demo:', err);
return;
}
fs.writeFile(`${nestedDirToDeletePath}/file_inside.txt`, 'Neki sadržaj', (err) => {
if (err) { console.error('Greška pri stvaranju datoteke unutar ugniježđenog direktorija:', err); return; }
console.log(`Direktorij '${nestedDirToDeletePath}' i datoteka stvoreni za demo brisanja.`);
fs.rm(nestedDirToDeletePath, { recursive: true, force: true }, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Greška pri brisanju rekurzivnog direktorija '${nestedDirToDeletePath}': ${err.message}`);
return;
}
console.log(`Rekurzivni direktorij '${nestedDirToDeletePath}' uspješno izbrisan.`);
});
});
});
// Brisanje praznog direktorija
fs.mkdir(dirToDeletePath, (err) => {
if (err && err.code !== 'EEXIST') {
console.error('Greška pri stvaranju praznog direktorija za demo:', err);
return;
}
console.log(`Direktorij '${dirToDeletePath}' stvoren za demo brisanja.`);
fs.rm(dirToDeletePath, { recursive: false }, (err: NodeJS.ErrnoException | null) => {
if (err) {
console.error(`Greška pri brisanju praznog direktorija '${dirToDeletePath}': ${err.message}`);
return;
}
console.log(`Prazan direktorij '${dirToDeletePath}' uspješno izbrisan.`);
});
});
Brisanje direktorija temeljeno na `Promise`-ima (`fs/promises`)
import * as fsPromises from 'fs/promises';
async function deleteDirectory(path: string, recursive: boolean = false): Promise
Napredni koncepti datotečnog sustava uz TypeScript
Osim osnovnih operacija čitanja/pisanja, Node.js nudi moćne značajke za rad s većim datotekama, kontinuiranim protocima podataka i praćenjem datotečnog sustava u stvarnom vremenu. TypeScriptove deklaracije tipova elegantno se proširuju na ove napredne scenarije, osiguravajući robusnost.
Datotečni deskriptori i streamovi
Za vrlo velike datoteke ili kada trebate preciznu kontrolu nad pristupom datotekama (npr. određene pozicije unutar datoteke), datotečni deskriptori i streamovi postaju ključni. Streamovi pružaju učinkovit način za rukovanje čitanjem ili pisanjem velikih količina podataka u dijelovima (chunks), umjesto učitavanja cijele datoteke u memoriju, što je ključno za skalabilne aplikacije i učinkovito upravljanje resursima na poslužiteljima globalno.
Otvaranje i zatvaranje datoteka s deskriptorima (`fs.open`, `fs.close`)
Datotečni deskriptor je jedinstveni identifikator (broj) koji operativni sustav dodjeljuje otvorenoj datoteci. Možete koristiti `fs.open` da biste dobili datotečni deskriptor, zatim izvršiti operacije poput `fs.read` ili `fs.write` koristeći taj deskriptor i na kraju ga zatvoriti s `fs.close`.
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
import { constants } from 'fs';
const descriptorFilePath: string = 'data/descriptor_example.txt';
async function demonstrateFileDescriptorOperations(): Promise
Datotečni streamovi (`fs.createReadStream`, `fs.createWriteStream`)
Streamovi su moćni za učinkovito rukovanje velikim datotekama. `fs.createReadStream` i `fs.createWriteStream` vraćaju `Readable` i `Writable` streamove, redom, koji se besprijekorno integriraju s Node.js streaming API-jem. TypeScript pruža izvrsne definicije tipova za događaje ovih streamova (npr. `'data'`, `'end'`, `'error'`).
import * as fs from 'fs';
const largeFilePath: string = 'data/large_file.txt';
const copiedFilePath: string = 'data/copied_file.txt';
// Stvorite probnu veliku datoteku za demonstraciju
function createLargeFile(path: string, sizeInMB: number): void {
const content: string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '; // 56 znakova
const stream = fs.createWriteStream(path);
const totalChars = sizeInMB * 1024 * 1024; // Pretvori MB u bajtove
const iterations = Math.ceil(totalChars / content.length);
for (let i = 0; i < iterations; i++) {
stream.write(content);
}
stream.end(() => console.log(`Stvorena velika datoteka '${path}' (${sizeInMB}MB).`));
}
// Za demonstraciju, prvo osigurajmo da 'data' direktorij postoji
fs.mkdir('data', { recursive: true }, (err) => {
if (err && err.code !== 'EEXIST') {
console.error('Greška pri stvaranju data direktorija:', err);
return;
}
createLargeFile(largeFilePath, 1); // Stvori datoteku od 1MB
});
// Kopirajte datoteku koristeći streamove
function copyFileWithStreams(source: string, destination: string): void {
const readStream = fs.createReadStream(source);
const writeStream = fs.createWriteStream(destination);
readStream.on('open', () => console.log(`Stream za čitanje '${source}' otvoren.`));
writeStream.on('open', () => console.log(`Stream za pisanje '${destination}' otvoren.`));
// Preusmjerite podatke iz read streama u write stream
readStream.pipe(writeStream);
readStream.on('error', (err: Error) => {
console.error(`Greška u streamu za čitanje: ${err.message}`);
});
writeStream.on('error', (err: Error) => {
console.error(`Greška u streamu za pisanje: ${err.message}`);
});
writeStream.on('finish', () => {
console.log(`Datoteka '${source}' uspješno kopirana u '${destination}' koristeći streamove.`);
// Očistite probnu veliku datoteku nakon kopiranja
fs.unlink(largeFilePath, (err) => {
if (err) console.error('Greška pri brisanju velike datoteke:', err);
else console.log(`Velika datoteka '${largeFilePath}' izbrisana.`);
});
});
}
// Pričekajte malo da se velika datoteka stvori prije pokušaja kopiranja
setTimeout(() => {
copyFileWithStreams(largeFilePath, copiedFilePath);
}, 1000);
Praćenje promjena: `fs.watch`, `fs.watchFile`
Praćenje promjena na datotečnom sustavu ključno je za zadatke poput "hot-reloadinga" razvojnih poslužitelja, procesa izgradnje (build) ili sinkronizacije podataka u stvarnom vremenu. Node.js pruža dvije primarne metode za to: `fs.watch` i `fs.watchFile`. TypeScript osigurava da su tipovi događaja i parametri slušača ispravno obrađeni.
`fs.watch`: Praćenje datotečnog sustava temeljeno na događajima
`fs.watch` je općenito učinkovitiji jer često koristi obavijesti na razini operativnog sustava (npr. `inotify` na Linuxu, `kqueue` na macOS-u, `ReadDirectoryChangesW` na Windowsima). Prikladan je za praćenje specifičnih datoteka ili direktorija za promjene, brisanja ili preimenovanja.
import * as fs from 'fs';
const watchedFilePath: string = 'data/watched_file.txt';
const watchedDirPath: string = 'data/watched_dir';
// Osigurajte da datoteke/direktoriji postoje za praćenje
fs.writeFileSync(watchedFilePath, 'Početni sadržaj.');
fs.mkdirSync(watchedDirPath, { recursive: true });
console.log(`Pratim '${watchedFilePath}' za promjene...`);
const fileWatcher = fs.watch(watchedFilePath, (eventType: string, filename: string | Buffer | null) => {
const fname = typeof filename === 'string' ? filename : filename?.toString('utf8');
console.log(`Događaj datoteke '${fname || 'N/A'}': ${eventType}`);
if (eventType === 'change') {
console.log('Sadržaj datoteke potencijalno promijenjen.');
}
// U stvarnoj aplikaciji, ovdje biste mogli pročitati datoteku ili pokrenuti ponovnu izgradnju
});
console.log(`Pratim direktorij '${watchedDirPath}' za promjene...`);
const dirWatcher = fs.watch(watchedDirPath, (eventType: string, filename: string | Buffer | null) => {
const fname = typeof filename === 'string' ? filename : filename?.toString('utf8');
console.log(`Događaj direktorija '${watchedDirPath}': ${eventType} na '${fname || 'N/A'}'`);
});
fileWatcher.on('error', (err: Error) => console.error(`Greška promatrača datoteke: ${err.message}`));
dirWatcher.on('error', (err: Error) => console.error(`Greška promatrača direktorija: ${err.message}`));
// Simulirajte promjene nakon odgode
setTimeout(() => {
console.log('\n--- Simuliram promjene ---');
fs.appendFileSync(watchedFilePath, '\nNova linija dodana.');
fs.writeFileSync(`${watchedDirPath}/new_file.txt`, 'Sadržaj.');
fs.unlinkSync(`${watchedDirPath}/new_file.txt`); // Također testirajte brisanje
setTimeout(() => {
fileWatcher.close();
dirWatcher.close();
console.log('\nPromatrači zatvoreni.');
// Očistite privremene datoteke/direktorije
fs.unlinkSync(watchedFilePath);
fs.rmSync(watchedDirPath, { recursive: true, force: true });
}, 2000);
}, 1000);
Napomena o `fs.watch`: Nije uvijek pouzdan na svim platformama za sve vrste događaja (npr. preimenovanja datoteka mogu biti prijavljena kao brisanja i stvaranja). Za robusno praćenje datoteka na više platformi, razmislite o knjižnicama poput `chokidar`, koje često koriste `fs.watch` ispod haube, ali dodaju normalizaciju i rezervne mehanizme.
`fs.watchFile`: Praćenje datoteka temeljeno na prozivanju (polling)
`fs.watchFile` koristi prozivanje (periodično provjeravanje `stat` podataka datoteke) za otkrivanje promjena. Manje je učinkovit, ali dosljedniji na različitim datotečnim sustavima i mrežnim pogonima. Bolje je prilagođen okruženjima gdje `fs.watch` može biti nepouzdan (npr. NFS dijeljenja).
import * as fs from 'fs';
import { Stats } from 'fs';
const pollFilePath: string = 'data/polled_file.txt';
fs.writeFileSync(pollFilePath, 'Početni prozivani sadržaj.');
console.log(`Prozivam '${pollFilePath}' za promjene...`);
fs.watchFile(pollFilePath, { interval: 1000 }, (curr: Stats, prev: Stats) => {
// TypeScript osigurava da su 'curr' i 'prev' fs.Stats objekti
if (curr.mtimeMs !== prev.mtimeMs) {
console.log(`Datoteka '${pollFilePath}' izmijenjena (mtime promijenjen). Nova veličina: ${curr.size} bajtova.`);
}
});
setTimeout(() => {
console.log('\n--- Simuliram promjenu prozivane datoteke ---');
fs.appendFileSync(pollFilePath, '\nJoš jedna linija dodana u prozivanu datoteku.');
setTimeout(() => {
fs.unwatchFile(pollFilePath);
console.log(`\nPrestao sam pratiti '${pollFilePath}'.`);
fs.unlinkSync(pollFilePath);
}, 2000);
}, 1500);
Obrada grešaka i najbolje prakse u globalnom kontekstu
Robusna obrada grešaka od presudne je važnosti za svaku aplikaciju spremnu za produkciju, posebno onu koja komunicira s datotečnim sustavom. Operacije s datotekama mogu propasti iz brojnih razloga: problemi s dozvolama, pun disk, datoteka nije pronađena, I/O greške, mrežni problemi (za mrežne pogone) ili sukobi istovremenog pristupa. TypeScript vam pomaže uhvatiti probleme vezane uz tipove, ali greške u vremenu izvođenja i dalje zahtijevaju pažljivo upravljanje.
Strategije obrade grešaka
- Sinkrone operacije: Uvijek omotajte `fs.xxxSync` pozive u `try...catch` blokove. Ove metode izravno bacaju greške.
- Asinkroni povratni pozivi: Prvi argument `fs` povratnog poziva uvijek je `err: NodeJS.ErrnoException | null`. Uvijek prvo provjerite ovaj `err` objekt.
- Temeljeno na `Promise`-ima (`fs/promises`): Koristite `try...catch` s `await` ili `.catch()` s `.then()` lancima za obradu odbijanja.
Korisno je standardizirati formate bilježenja grešaka i razmotriti internacionalizaciju (i18n) za poruke o greškama ako je povratna informacija o greškama vaše aplikacije namijenjena korisnicima.
import * as fs from 'fs';
import { promises as fsPromises } from 'fs';
import * as path from 'path';
const problematicPath = path.join('non_existent_dir', 'file.txt');
// Sinkrona obrada grešaka
try {
fs.readFileSync(problematicPath, 'utf8');
} catch (error: any) {
console.error(`Sinkrona greška: ${error.code} - ${error.message} (Putanja: ${problematicPath})`);
}
// Obrada grešaka temeljena na povratnom pozivu
fs.readFile(problematicPath, 'utf8', (err, data) => {
if (err) {
console.error(`Greška u povratnom pozivu: ${err.code} - ${err.message} (Putanja: ${problematicPath})`);
return;
}
// ... obradi podatke
});
// Obrada grešaka temeljena na Promise-u
async function safeReadFile(filePath: string): Promise
Upravljanje resursima: Zatvaranje datotečnih deskriptora
Kada radite s `fs.open` (ili `fsPromises.open`), ključno je osigurati da se datotečni deskriptori uvijek zatvore pomoću `fs.close` (ili `fileHandle.close()`) nakon što su operacije završene, čak i ako dođe do grešaka. Ako to ne učinite, može doći do curenja resursa, dostizanja ograničenja otvorenih datoteka operativnog sustava i potencijalnog rušenja vaše aplikacije ili utjecaja na druge procese.
API `fs/promises` s `FileHandle` objektima općenito pojednostavljuje ovo, budući da je `fileHandle.close()` specifično dizajniran za tu svrhu, a instance `FileHandle` su `Disposable` (ako koristite Node.js 18.11.0+ i TypeScript 5.2+).
Upravljanje putanjama i kompatibilnost s različitim platformama
Putanje datoteka značajno se razlikuju između operativnih sustava (npr. `\` na Windowsima, `/` na Unix-sličnim sustavima). Node.js `path` modul je neophodan za izgradnju i parsiranje putanja datoteka na način kompatibilan s različitim platformama, što je ključno za globalne implementacije.
- `path.join(...paths)`: Spaja sve zadane segmente putanje, normalizirajući rezultirajuću putanju.
- `path.resolve(...paths)`: Rješava niz putanja ili segmenata putanje u apsolutnu putanju.
- `path.basename(path)`: Vraća zadnji dio putanje.
- `path.dirname(path)`: Vraća ime direktorija iz putanje.
- `path.extname(path)`: Vraća ekstenziju iz putanje.
TypeScript pruža potpune definicije tipova za `path` modul, osiguravajući da njegove funkcije koristite ispravno.
import * as path from 'path';
const dir = 'my_app_data';
const filename = 'config.json';
// Spajanje putanja kompatibilno s više platformi
const fullPath: string = path.join(__dirname, dir, filename);
console.log(`Putanja za više platformi: ${fullPath}`);
// Dohvati ime direktorija
const dirname: string = path.dirname(fullPath);
console.log(`Ime direktorija: ${dirname}`);
// Dohvati osnovno ime datoteke
const basename: string = path.basename(fullPath);
console.log(`Osnovno ime: ${basename}`);
// Dohvati ekstenziju datoteke
const extname: string = path.extname(fullPath);
console.log(`Ekstenzija: ${extname}`);
Istovremenost i "Race Conditions"
Kada se više asinkronih operacija s datotekama pokrene istovremeno, posebno pisanja ili brisanja, mogu se pojaviti "race conditions". Na primjer, ako jedna operacija provjerava postojanje datoteke, a druga je izbriše prije nego što prva operacija djeluje, prva operacija može neočekivano propasti.
- Izbjegavajte `fs.existsSync` za kritičnu logiku; preferirajte `fs.access` ili jednostavno pokušajte izvršiti operaciju i obradite grešku.
- Za operacije koje zahtijevaju isključivi pristup, koristite odgovarajuće opcije `flag` (npr. `'wx'` za isključivo pisanje).
- Implementirajte mehanizme zaključavanja (npr. zaključavanje datoteka ili zaključavanje na razini aplikacije) za visoko kritičan pristup dijeljenim resursima, iako to dodaje složenost.
Dozvole (ACL)
Dozvole datotečnog sustava (Liste za kontrolu pristupa ili standardne Unix dozvole) čest su izvor grešaka. Osigurajte da vaš Node.js proces ima potrebne dozvole za čitanje, pisanje ili izvršavanje datoteka i direktorija. To je posebno važno u kontejneriziranim okruženjima ili na višekorisničkim sustavima gdje se procesi izvršavaju s određenim korisničkim računima.
Zaključak: Prihvaćanje sigurnosti tipova za globalne operacije s datotečnim sustavom
Node.js `fs` modul moćan je i svestran alat za interakciju s datotečnim sustavom, nudeći spektar opcija od osnovnih manipulacija datotekama do napredne obrade podataka temeljene na streamovima. Dodavanjem TypeScripta povrh ovih operacija, dobivate neprocjenjive prednosti: otkrivanje grešaka pri kompajliranju, poboljšanu jasnoću koda, vrhunsku podršku alata i povećano samopouzdanje tijekom refaktoriranja. To je posebno ključno za globalne razvojne timove gdje su dosljednost i smanjena dvosmislenost u različitim bazama koda od vitalnog značaja.
Bilo da gradite malu pomoćnu skriptu ili veliku poslovnu aplikaciju, korištenje TypeScriptovog robusnog sustava tipova za vaše Node.js operacije s datotekama dovest će do koda koji je lakši za održavanje, pouzdaniji i otporniji na greške. Prihvatite `fs/promises` API za čišće asinkrone obrasce, razumijte nijanse između sinkronih i asinkronih poziva i uvijek dajte prioritet robusnoj obradi grešaka i upravljanju putanjama na više platformi.
Primjenom principa i primjera o kojima se raspravljalo u ovom vodiču, programeri diljem svijeta mogu izgraditi interakcije s datotečnim sustavom koje nisu samo performantne i učinkovite, već i inherentno sigurnije i lakše za razumijevanje, što u konačnici doprinosi isporuci softvera više kvalitete.